//
//  TypeBuilder
//
//  Created by Caue W. on 2011-03-15.
//  Copyright (c) 2011 __MyCompanyName__. All rights reserved.
//
package time.compiler.types;
private typedef StdType = Type;

import haxe.macro.Context;
import haxe.macro.Expr;
import haxe.macro.Type;
import time.Errors;
import time.compiler.types.Data;
using Lambda;
using time.compiler.types.Data;

class TypeBuilder 
{
	
	public static function getData(path:String):DBType
	{
		var t = tryGetData(path);
		if (t == null)
			throw TypeNotFound(path);
		return t;
	}
	
	public static function tryGetData(path:String):Null<DBType>
	{
		var t = Context.getType(path);
		if (null == t)
			return null;
		
		return defaultInstance.getInfos(t);
	}
	
	public static function convertToDb(t:Type):DBType
	{
		return defaultInstance.getInfos(t);
	}
	
	static var defaultInstance:TypeBuilder = new TypeBuilder();
	static var keyType(get_keyType, null):DBType;
	static function get_keyType()
	{
		return if (keyType != null) keyType else getData("time.types.ds.Key");
	}
	
	var allTypeBuilder:Hash<{type:DBModule, complete:Bool}>;
	
	var visitedTypeBuilder:Hash<haxe.macro.Type>;
	var currentDBType:Null<DBType>;
	var current:Type;
	var checks:Array<Void->Void>;
	var pos:Null<Position>;
	
	public function new()
	{
		allTypeBuilder = new Hash();
		visitedTypeBuilder = new Hash();
		checks = [];
		
		Context.onGenerate(function(a) doChecks(a));
	}
	
	function doChecks(t:Array<Type>)
	{
		//TODO recursively check parent/child blurry relations
		
		while (checks.length > 0)
			checks.pop()();
	}
	
	public function convertModule(m:DBModule):DBType
	{
		return switch(m)
		{
			case DBClassDecl(i):
				DBInst(i);
			case DBEnumDecl(e):
				DBEnum(e);
		}
	}
	
	public function getInfos(t:haxe.macro.Type) : DBType
	{
		var lastDBType = currentDBType;
		var last = current;
		
		return switch(current = t)
		{
			case TMono(_): throw TypeNotFound("TMono");
			case TEnum(t, _):
				var path = t.toString();
				return if (path == "Bool")
				{
					DBBasic(BBool);
				} else if (allTypeBuilder.exists(path))
				{
					convertModule(allTypeBuilder.get(path).type);
				} else {
					var enumInfo = {type:HValueType /*enums are always Value Type*/, path:path, dbPath:path, members:new Hash(), ctors:[], fields:new Hash(), builtMembers:[] };
					var t = t.get();
					var processedType = {type:DBEnumDecl(enumInfo), complete:false};
					allTypeBuilder.set(path, processedType); //ensure that we don't go on an infinite loop with circular references
					var lastDBType = currentDBType;
					currentDBType = DBEnum(enumInfo);
					
					var types:DBTypeContainer<Array<Member>> = new DBTypeContainer();
					
					//Member = { type: MValueType/MRelation/MSerialized, name, dbName, defaults:DNow/DString/DInt/DFloat/DInt64/DBigInt/DDecimal}
					var ctorNum = 0;
					for (ctor in t.names)
					{
						var curTypes:DBTypeContainer<Int> = new DBTypeContainer();
						
						var e = t.constructs.get(ctor);
						
						var members = [];
						var metas = new Hash();
						var globalMetas = [];
						
						//grabbing all metas that might be applied to a specific member
						for (meta in e.meta.get())
						{
							if (meta.params != null && meta.params.length > 0)
							{
								if (meta.name == "ignore" || meta.name == ":ignore")
									throw "Cannot make ignored fields on enums";
								
								switch(meta.params[0].expr)
								{
									case EConst(c):
										switch(c)
										{
											case CString(s), CIdent(s), CType(s):
												var m = metas.get(s);
												if (m == null)
												{
													m = [];
													metas.set(s, m);
												}
												
												m.push({name:meta.name, params:[], pos:meta.pos});
											default:
												globalMetas.push(meta);
										}
									default:
										globalMetas.push(meta);
								}
							} else {
								globalMetas.push(meta);
							}
						}
						
						switch(e.type)
						{
							case TFun(args, _):
								var i = 0;
								for (arg in args)
								{
									var allmeta = metas.get(arg.name);
									if (allmeta == null) allmeta = [];
									allmeta = allmeta.concat(globalMetas);
									
									var m = getMember(ctor + "_" + i, ctor + "_" + i, arg.t, allmeta);
									if (m == null) //TODO can we make ignored fields in a nice way (without breaking decoding)? I don't think we can
										throw "Cannot make ignored fields on enums";
									
									var t = curTypes.get(m.type);
									if (null == t)
									{
										t = 0;
									}
									m.dbName = m.name = m.type.toNormalizedString() + "_" + t;
									
									//TODO Different kinds for the types here might break pretty badly this logic. FIXME
									var allt = types.get(m.type);
									if (null == allt)
									{
										allt = [];
										types.add(m.type, allt);
									}
									
									if (allt.length < t)
									{
										allt[t] = m;
										enumInfo.builtMembers.push(m);
									}
									
									members.push(m);
									var name = arg.name;
									if (enumInfo.fields.exists(name))
									{
										trace("WARNING: " + name + " on enum already exists. Completion for field access will not work properly for this type @ " + e.pos); //TODO replace for a proper warning
										var i =0;
										do
										{
											name = arg.name + "_" + i;
										} while (enumInfo.fields.exists(name));
									}
									
									enumInfo.fields.set(name, {ctor:ctor, idx:i, member:m});
									curTypes.add(m.type, t + 1);
									
									i++;
								}
							case TEnum(_,_): //no members
								
							default:
								throw Assert;
						}
						
						enumInfo.ctors.push(ctor);
						enumInfo.members.set(ctor, members);
						
						ctorNum++;
					}
					
					processedType.complete = true;
					var c = currentDBType;
					currentDBType = lastDBType;
					
					return c;
				}
			case TInst(i, p):
				var path = i.toString();
				return if (allTypeBuilder.exists(path))
				{
					convertModule(allTypeBuilder.get(path).type);
				} else {
					
					switch(path)
					{
						case "time.types.ds.Key":
							return DBBasic(BKey(this.getInfos(p[0])));
						case "Int":
							return DBBasic(BInt);
						case "haxe.Int32":
							return DBBasic(BInt32);
						case "haxe.Int64":
							return DBBasic(BInt64);
						case "Float":
							return DBBasic(BDouble);
						case "Date":
							return DBBasic(BDate);
						case "String":
							return DBBasic(BText);
						case "haxe.io.Bytes":
							return DBBasic(BBlob);
						case "Array", "List":
							return DBToMany(getInfos(p[0]));
						default:
							if (StringTools.startsWith(path, "haxe.data.collections"))
								return DBToMany(getInfos(p[0]));
					}
					
					var instInfo = {type:HUnknown, path:path, dbPath:path, idField:"", orderedMembers:[], members:new Hash(), superClass:null, isWrapper:null, indices:[] };
					var i = i.get();
					var processedType = {type:DBClassDecl(instInfo), complete:false};
					allTypeBuilder.set(path, processedType); //ensure that we don't go on an infinite loop with circular references
					var lastDBType = currentDBType;
					currentDBType = DBInst(instInfo);
					
					if (i.superClass != null)
					{
						var sc = getInfos( TInst(i.superClass.t, i.superClass.params) );
						switch(sc)
						{
							case DBInst(i): instInfo.superClass = i;
							default: throw Assert;
						}
					}
					
					for (m in i.meta.get())
					{
						function metaSize(val:Int)
						{
							if (m.params.length != val)
								throw MetadataError("Wrong metadata '" + m.name + "' arguments number: Expected " + val + ", got " + m.params.length, m.pos);
						}
						
						function metaType(expected)
						{
							throw MetadataError("Wrong metadata '" + m.name + "' type; Expected " + expected, m.pos);
							return null;
						}
						
						switch(m.name)
						{
							case ":rename", "rename":
								metaSize(1);
								
								instInfo.dbPath = switch(m.params[0].expr)
								{
									case EConst(c):
										switch(c)
										{
											case CIdent(s), CString(s), CType(s): s;
											default: metaType("String");
										}
									default: metaType("String");
								}
							case ":valueType", "valueType":
								metaSize(0);
							
								instInfo.type = HValueType;
								//TODO look for any own key reference and send an error message
						}
					}
					
					for (field in i.fields.get())
					{
						switch(field.kind)
						{
							case FVar(g,_): //only works with variables
								switch(g)
								{
									case AccNever: continue; //ignore -never- getter variables, as they are not real variables
									default:
								}
								
								var m = getMember(field.name, field.name, field.type, field.meta.get());
								if (m == null)
									continue;
								
								instInfo.orderedMembers.push(m);
								instInfo.members.set(m.name, m);
							default: //ignore functions
						}
					}
					
					processedType.complete = true;
					var c = currentDBType;
					currentDBType = lastDBType;
					
					return c;
				}
				
			case TType(t, p):
				var path = t.toString();
				return switch(path)
				{
					case "time.Text":
						DBBasic(BText);
					case "time.Varchar":
						switch(p[0])
						{
							case TInst(ti, _):
								DBBasic(BVarchar(getIntFromConstParam(ti)));
							default: throw Assert;
						}
					case "time.Blob":
						DBBasic(BBlob);
					case "time.VarBlob":
						switch(p[0])
						{
							case TInst(ti, _):
								DBBasic(BVarBlob(getIntFromConstParam(ti)));
							default: throw Assert;
						}
					default:
						getInfos(Context.follow(current, true));
				}
			case TFun(_,_), TAnonymous(_), TDynamic(_):
				throw UnsupportedType(current);
		}
	}
	
	function getMember(name:String, dbName:String, type:Type, metas:Metadata, ?pos:Null<Position>) : Null<Member>
	{
		//ok, here we will perform countless checks to determine if member type is Reference, Value Type, Child, Parent, Serialized or To Many
		//also for each specific type, we need to test for some cases:
		// - Reference Type: a reference type shall only refer to a Root type
		// - Value Type: the underlying type should be HValueType or HChild, and look for circular references 
		// - Child: the underlying type should be HChild, and look for complex circular references. To do that, need to traverse the objects and keep reference of them
		// - Parent: can be either HRoot or HChild, and will look for complex circular references (the same class being referenced as both parent and child)
		// - Serialized: will return an opaque type. No check will be performed - the underlying type can even not be a db-ready type.
		// - To Many: To recognize To Many, we have to check a dynamic list of types that are collections, with also their code to 
		//  re-create the type out of an iterator. To-many relations can only be in reference to child class types (this avoids complex arrays of arrays and
		//  references to un-referenceable types (e.g. ValueTypeBuilder / Basic TypeBuilder). But there are ways to circumvent this limitation (Wrap<> special type)
		//  
		
		var member = { kind:null, type:null, name:name, dbName:dbName};
		
		var immediateChecks = [];
		
		//TODO create setKind function
		function setKind(k:MemberKind)
		{
			if (null != member.kind)
			{
				switch(member.kind)
				{
					case MValueType, MSerialized, MReference(_,_), MBackReference(_,_,_): if (StdType.enumIndex(member.kind) != StdType.enumIndex(k)) throw IncompatibleKind(member.kind, k, pos);
					case MRelation(t): switch(k) { case MRelation(t2): if (StdType.enumIndex(t) != StdType.enumIndex(t2)) throw IncompatibleKind(member.kind, k, pos); default: throw IncompatibleKind(member.kind, k, pos); }
				}
			}
			member.kind = k;
		}
		
		for (m in metas)
		{
			function metaSize(val:Int)
			{
				if (m.params.length != val)
					throw MetadataError("Wrong metadata '" + m.name + "' arguments number: Expected " + val + ", got " + m.params.length, m.pos);
			}
			
			function metaType(expected)
			{
				throw MetadataError("Wrong metadata '" + m.name + "' type; Expected " + expected, m.pos);
				return null;
			}
			
			function getString(param, ?acceptIdent = false) : String
			{
				return switch(param.expr)
				{
					case EConst(c):
						switch(c)
						{
							case CString(s): s;
							case CIdent(s), CType(s): if (acceptIdent) s; else metaType("String");
							default: metaType("String");
						}
					default: metaType("String");
				};
			}
			
			switch(m.name)
			{
				case ":rename", "rename":
					metaSize(1);
					dbName = getString(m.params[0], true);
				
				case ":serialize", "serialize":
					metaSize(0);
					setKind(MSerialized);
					
					if (member.type == null)
						member.type = DBBasic(BSerializedType);
					else
						throw MetadataError("Error on setting type as serialized: Type is already defined as " + member.type, m.pos);
				
				case ":embed", "embed":
					metaSize(0);
					setKind(MValueType);
					
					//TODO add check if the underlying type references or has references to its keys
					
					setAsChild(this.currentDBType, member.type = this.getInfos(type), name, true);
				case ":ignore", "ignore":
					return null; //this member should be ignored
				
				case ":key", "key":
					var cur = this.currentDBType;
					
					var range = "asc";
					if (m.params[0] != null)
						range = getString(m.params[0]);
					
					switch(cur)
					{
						case DBInst(i):
							switch(range)
							{
								case "point": i.indices.push(IPoint([name]));
								case "asc": i.indices.push(IRange([name], true));
								case "desc": i.indices.push(IRange([name], false));
								default: throw MetadataError("Unknown command for id meta '" + range + "'.", m.pos);
							}
						default: throw MetadataError("cannot set id of non-class type", m.pos);
					}
					
					checks.push(function()
					{
						switch(member.type.follow())
						{
							case DBBasic(b): switch(b) { case BKey(_): /*ok*/ default: throw MetadataError("Only basic types can be set as keys.", m.pos); }
							default: throw MetadataError("Only basic types can be set as keys.", m.pos);
						}
					});
				
				case ":parent", "parent":
					var parentField = null;
					
					//check if the parent field is determined
					if (m.params.length > 0)
					{
						parentField = getString(m.params[0]);
					} else {
						var curType = this.currentDBType;
						
						//if it is not, we need to get this info at the end of the phase
						checks.push(function()
						{
							switch(curType.follow())
							{
								case DBInst(i):
									var parent = switch(i.type)
									{
										case HValueType: throw MetadataError("A Value Type cannot reference its parent", m.pos);
										case HUniqueChild(p): p;
										case HChild(ps): 
											if (ps == null) null; else {
												if (ps.length > 1) throw MetadataError("A child type with two different parents ( " + ps + " ) cannot reference its parent", m.pos);
												i.type = HUniqueChild(ps[0]);
												ps[0];
											}
										case HUnknown, HRoot: null;
									}
									
									if (parent == null)
										throw RttiError("Invalid parent metadata: Cannot find parent class.", m.pos);
									
									var member = switch(parent.type.follow())
									{
										case DBInst(i):
											i.members.get(parent.parentField);
										default: throw Assert;
									}
									
									if (member == null)
									{
										throw "Inexistent field @" + parent.type.toString() + "::" + parent.parentField;
									}
									
									switch(member.kind)
									{
										case MRelation(rel):
											switch(rel)
											{
												case ParentToChild:
													setKind(MRelation(ChildToParent(parent.parentField)));
												case ParentToChildren:
													setKind(MRelation(ChildrenToParent(parent.parentField)));
												default: throw Assert;
											}
										default: throw Assert;
									}
									
									
								default: throw Assert;
							}
						});
						
						
					}
					
					setKind(MRelation(ChildToParent(parentField)));
				case ":referenced", "referenced":
					var referencedType = getData(getString(m.params[0]));
					var referenceTo = getString(m.params[1]);
					
					var isToMany = false;
					switch(referencedType.follow())
					{
						case DBToMany(t): referencedType = t; isToMany = true;
						default:
					}
					
					setKind(MBackReference(referencedType, referenceTo, null));
					
					//TODO ensure to-many relation
					var curType = this.currentDBType;
					checks.push(function(){
						switch(referencedType.follow())
						{
							case DBInst(i):
								//check if type exists
								var member = i.members.get(referenceTo);
								if (member == null)
									throw FieldNotFound(referencedType, referenceTo);
								
								//check if the member is a reference
								switch(member.kind)
								{
									case MReference(to, opts):
										//check if referenced type matches our current type
										if (!curType.equals(member.type))
											throw MetadataError("Field '" + referenceTo + "' from type " + referencedType.toString() + " is not of type " + curType.toString, m.pos);
										opts.hasBackReference = true;
										opts.isBackReferenceToMany = isToMany;
										
										setKind(MBackReference(referencedType, referenceTo, opts));
									default:
										throw MetadataError("Field '" + referenceTo + "' from type " + referencedType.toString() + " is not a reference.", m.pos);
								}
							default:
								throw MetadataError("Type " + referencedType.toString() + " cannot have back referenced, since it is not a Class type", m.pos);
						}
					});
					
			}
		}
		
		//ok, we've gone through all metadata. Now we're going to identify the type
		
		if (member.type == null)
		{
			member.type = this.getInfos(type);
		}
		
		var valueType = if (member.kind != null)
		{
			switch(member.kind)
			{
				case MValueType: true;
				default: false;
			}
		} else false;
		
		var kind = setAsChild(this.currentDBType, member.type, member.name, valueType);
		
		setKind(kind);
		
		//TODO recursively check for illogical parent/child relationships
		
		for (check in immediateChecks) check();
		
		return member;
	}
	
	function setAsChild(parent:DBType, child:DBType, parentField:String, ?valueType:Bool = false, ?toMany:Bool = false):MemberKind
	{
		var kind = null;
		
		switch(child = child.follow())
		{
			case DBBasic(b):
				switch(b)
				{
					case BKey(to):
						
						kind = MReference(to, { isToMany: toMany, hasBackReference:null, isBackReferenceToMany:false });
						ensureCanBeReferenced(to);
						valueType = true;
					default:
						kind = MValueType;
						valueType = true;
				}
			case DBEnum(_): kind = (toMany) ? MRelation(ParentToChildren) : MValueType; valueType = true; //all ok
			case DBToMany(t):
				if (valueType)
					throw CannotBeValueType(child, pos); //TODO support Embedded Tiny Array
				if (toMany)
					throw InvalidType(DBToMany(child), pos);
				return setAsChild(parent, t, parentField, valueType, true);
			case DBInst(i):
				switch(i.type)
				{
					case HUnknown: i.type = HChild([{type:parent, valueType:valueType, parentField:parentField}]);
					case HValueType: valueType = true;  //if it's not marked as value type, it will be anyway
					case HUniqueChild(p): 
						if (null == p)
							i.type = HUniqueChild( { type:parent, parentField:parentField, valueType:valueType } );
						else
							throw UniqueChildMultipleParentsError(child, [p, {type:parent, valueType:valueType, parentField:parentField}], pos);
					case HChild(a): a.push({type:parent, valueType:valueType, parentField:parentField});
					case HRoot: i.type = HUniqueChild({type:parent, parentField:parentField, valueType:valueType}); //any root can be converted to HUniqueChild
				}
				
				if (toMany)
					kind = MRelation(ParentToChildren);
				else if (valueType)
					kind = MValueType;
				else
					kind = MRelation(ParentToChild);
			case DBLazy(_): throw Assert;
		}
		
		return kind;
	}
	
	function ensureCanBeReferenced(t:DBType) : Void
	{
		switch(t = t.follow())
		{
			case DBInst(i):
				switch(i.type)
				{
					case HUnknown, HRoot:
						i.type = HUniqueChild(null); //meaning: it must be a unique child, and must have a parent (still tbd) TODO: check for nullity on last step
					case HChild(p): if (p.length > 1) 
					{
						throw UniqueChildMultipleParentsError(t, p, pos);
					} else {
						i.type = HUniqueChild(p[0]);
					}
					case HUniqueChild(_): //ok, it can be referenced
					case HValueType: throw TypeCannotBeReferenced(t, true, pos);
				}
			case DBToMany(_): throw TypeCannotBeReferenced(t, false, pos);
			case DBEnum(_), DBBasic(_): throw TypeCannotBeReferenced(t, true, pos);
			case DBLazy(_): throw Assert;
		}
	}
	
	function getCache(path:String) : Null<haxe.macro.Type>
	{
		return visitedTypeBuilder.get(path);
	}
	
	function getIntFromConstParam(param:Dynamic) : Int
	{
		var s = Std.string(param);
		if (StringTools.startsWith(s, "I"))
			return Std.parseInt(s.substr(1));
		
		throw "Invalid Int const parameter";
	}
	
	
}